/******************************************************************************* * Copyright (c) 2005, 2015 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.ui.operations; import java.util.ArrayList; import org.eclipse.core.commands.operations.IAdvancedUndoableOperation; import org.eclipse.core.commands.operations.IOperationApprover; import org.eclipse.core.commands.operations.IOperationHistory; import org.eclipse.core.commands.operations.IUndoContext; import org.eclipse.core.commands.operations.IUndoableOperation; import org.eclipse.core.runtime.Adapters; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.osgi.util.NLS; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.internal.WorkbenchMessages; /** * <p> * An operation approver that prompts the user to see if a non-local undo should * proceed inside an editor. A non-local undo is detected when an operation * being undone or redone affects elements other than those described by the * editor itself. Clients can optionally specify a class, the preferred * comparison class, that should be used when comparing objects affected by the * editor with objects affected by an undo or redo operation. Comparisons * between the affected objects inside the editor and those described by the * operation will first be done by simply performing an equality check, using * {@link java.lang.Object#equals(java.lang.Object)}. If an object described by * an operation is not equal to one of the objects affected by the editor, and * if it is not an instance of the preferred comparison class, but is an * instance of {@link org.eclipse.core.runtime.IAdaptable}, then the operation * approver will also attempt to retrieve an adapter on that object for the * preferred comparison class and perform a second equality check using the * adapter. * </p> * <p> * This class may be instantiated by clients. * </p> * * * @since 3.1 */ public final class NonLocalUndoUserApprover implements IOperationApprover { private IUndoContext context; private IEditorPart part; private Object[] elements; private Class affectedObjectsClass; private ArrayList elementsAndAdapters; /** * Create a NonLocalUndoUserApprover associated with the specified editor * and undo context * * @param context * the undo context of operations in question. * @param part * the editor part that is displaying the element * @param affectedObjects * the objects that are affected by the editor and considered to * be objects local to the editor. The objects are typically * instances of the preferredComparisonClass or else provide * adapters for the preferredComparisonClass, although this is * not required. * @param preferredComparisonClass * the preferred class to be used when comparing the editor's * affectedObjects with those provided by the undoable operation * using * {@link org.eclipse.core.commands.operations.IAdvancedUndoableOperation#getAffectedObjects()}. * If the operation's affected objects are not instances of the * specified class, but are instances of * {@link org.eclipse.core.runtime.IAdaptable}, then an adapter * for this class will be requested. The preferredComparisonClass * may be <code>null</code>, which indicates that there is no * expected class or adapter necessary for the comparison. */ public NonLocalUndoUserApprover(IUndoContext context, IEditorPart part, Object[] affectedObjects, Class preferredComparisonClass) { super(); this.context = context; this.part = part; this.affectedObjectsClass = preferredComparisonClass; this.elements = affectedObjects; } @Override public IStatus proceedRedoing(IUndoableOperation operation, IOperationHistory history, IAdaptable uiInfo) { // return immediately if the operation is not relevant if (!requiresApproval(operation, uiInfo)) { return Status.OK_STATUS; } String message = NLS.bind( WorkbenchMessages.Operations_nonLocalRedoWarning, operation .getLabel(), part.getEditorInput().getName()); return proceedWithOperation(operation, message, WorkbenchMessages.Operations_discardRedo, WorkbenchMessages.Workbench_redoToolTip); } @Override public IStatus proceedUndoing(IUndoableOperation operation, IOperationHistory history, IAdaptable uiInfo) { // return immediately if the operation is not relevant if (!requiresApproval(operation, uiInfo)) { return Status.OK_STATUS; } String message = NLS.bind( WorkbenchMessages.Operations_nonLocalUndoWarning, operation .getLabel(), part.getEditorInput().getName()); return proceedWithOperation(operation, message, WorkbenchMessages.Operations_discardUndo, WorkbenchMessages.Workbench_undoToolTip); } /* * Determine whether the operation in question affects elements outside of * the editor. If this can be determined and it does affect other elements, * prompt the user as to whether the operation should proceed. */ private IStatus proceedWithOperation(IUndoableOperation operation, final String message, final String discardButton, final String title) { // if the operation cannot tell us about its modified elements, there's // nothing we can do. if (!(operation instanceof IAdvancedUndoableOperation)) { return Status.OK_STATUS; } // Obtain the operation's affected objects. Object[] modifiedElements = ((IAdvancedUndoableOperation) operation) .getAffectedObjects(); // Since the operation participates in describing its affected objects, // we assume for the rest of this method that an inability to // determine a match implies that a non-local operation is occurring. // This is a conservative assumption that provides more user prompting. boolean local; if (modifiedElements == null) { // The operation could not determine which elements are affected. // Consider the operation non-local. local = false; } else { // The operation answered some array of affected objects. Consider // the operation local until a non-match is found. Note that an // empty // array of affected objects is considered a local change. local = true; for (Object modifiedElement : modifiedElements) { if (!elementsContains(modifiedElement)) { // the modified element is not known by the editor local = false; // one last try - try to adapt the modified element if a // preferred // comparison class has been provided. if (affectedObjectsClass != null) { Object adapter = Adapters.adapt(modifiedElement, affectedObjectsClass); if (adapter != null && elementsContains(adapter)) { local = true; } } // if the element did not match the affected objects, no // need to check any others. if (!local) { break; } } } } if (local) { return Status.OK_STATUS; } // The operation affects more than just our element. Find out if // we should proceed, cancel, or discard the undo. Must be done in // a syncExec because operation approval notifications may come from // a background thread. final int[] answer = new int[1]; PlatformUI.getWorkbench().getDisplay().syncExec(() -> { MessageDialog dialog = new MessageDialog(part.getSite().getShell(), title, null, message, MessageDialog.QUESTION, 0, IDialogConstants.OK_LABEL, discardButton, IDialogConstants.CANCEL_LABEL); // yes is the default answer[0] = dialog.open(); }); switch (answer[0]) { case 0: return Status.OK_STATUS; case 1: return IOperationHistory.OPERATION_INVALID_STATUS; default: // Cancel by default to include ESC key and shell close, // which return SWT.DEFAULT, and any other unexpected return codes return Status.CANCEL_STATUS; } } /* * Answer whether this operation is relevant enough to this operation * approver that it should be examined in detail. */ private boolean requiresApproval(IUndoableOperation operation, IAdaptable uiInfo) { // no approval is required if the operation doesn't have our undo // context if (!(operation.hasContext(context))) { return false; } // no approval is required if the operation only has our context if (operation.getContexts().length == 1) { return false; } // no approval is required if we can ascertain that the operation did // not originate // in our context. if (uiInfo != null) { IUndoContext originatingContext = Adapters.adapt(uiInfo, IUndoContext.class); if (originatingContext != null && !(originatingContext.matches(context))) { return false; } } return true; } /* * Return whether or not the collection of editor elements plus any of their * adapters contains the specified object. */ private boolean elementsContains(Object someObject) { if (elements == null) { return false; } if (elementsAndAdapters == null) { // Compute a list of not just the elements, but any adapters they // may provide on the preferred class if they are not instances of // the preferred class. This is done only once. elementsAndAdapters = new ArrayList(elements.length); for (Object element : elements) { elementsAndAdapters.add(element); if (affectedObjectsClass != null && !affectedObjectsClass.isInstance(element)) { Object adapter = Adapters.adapt(element, affectedObjectsClass); if (adapter != null) { elementsAndAdapters.add(adapter); } } } } for (int i = 0; i < elementsAndAdapters.size(); i++) { if (elementsAndAdapters.get(i).equals(someObject)) { return true; } } return false; } }